package ru.dc;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import ru.dc.object.commands.ChatCommand;
import ru.dc.object.commands.Command;
import ru.dc.object.CommandListener;
import ru.dc.object.ServerInfo;
import ru.dc.object.commands.CommandFactory;

import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 * <pre>
 * User: Penkov Vladimir
 * Date: 08.10.2010
 * Time: 15:44:39
 * </pre>
 */
public class Connection {

    final Object reconnectLock = new Object();
    private ServerInfo serverInfo;
    private boolean connected;
    private Log logger = LogFactory.getLog(getClass());
    private Socket socket;
    private List<CommandListener> commandListeners = new ArrayList<CommandListener>();
    private OutputStream out;
    private boolean reconnectOnError = true;
    private BufferedInputStream in;
    private long last;
    private StringBuilder stringBuffer;
    public static final int BUFFER_SIZE = 1024*10*10;
    public static final int RECONNECTING_INTERVAL = 5000;
    private boolean terminating = false;


    public synchronized void connect() throws IOException {
        if (isConnected()) {
            return;
        }
        if (serverInfo==null) {
            logger.warn("Server information needs to be set before connecting");
            return;
        }

        if (isConnected()) {
            logger.warn("Already connected to "+serverInfo);
            return;
        }

        logger.debug("Connecting to: "+serverInfo);
        beforeConnect();
        try {
            socket = createSocket();
        } catch (IOException e) {
            onSocketCreateError(e);
            socket = null;
        }

        if (socket!=null) {
            connected = true;
            evalSocket();
            afterConnect();
            last = System.currentTimeMillis();
            logger.info("Connected to "+serverInfo);
        }
    }

    protected Socket createSocket() throws IOException {
        return new Socket(serverInfo.getAddress(), serverInfo.getPort());
    }

    protected void onSocketCreateError(IOException e) {
        logger.error("Can't open socket: "+serverInfo, e);
    }

    protected void beforeConnect() {
    }

    protected void afterConnect() {
    }

    protected void evalSocket() throws IOException {
        socket.setKeepAlive(true);
        out = socket.getOutputStream();
        in = new BufferedInputStream(socket.getInputStream());
        Thread readingThread = new Thread() {
            @Override
            public void run() {
                stringBuffer = new StringBuilder();

                try {


                    byte[] buffer = new byte[BUFFER_SIZE];
                    int readen;
                    while ((readen = in.read(buffer))!=-1) {
                        parseReadenData(buffer, readen);
                    }

                    logger.trace("Recieved EOF for "+serverInfo);

                } catch (IOException e) {
                    logger.error("Error reading data from connection to "+serverInfo, e);
                    if (reconnectOnError) {
                        try {
                            reconnect();
                        } catch (IOException e1) {
                            logger.error("Can't reconnect to "+serverInfo, e);
                        }
                    }
                }
                logger.trace("Exiting reading thread for "+serverInfo);
            }
        };
        readingThread.start();
    }

    protected void parseReadenData(byte[] buffer, int readen) throws UnsupportedEncodingException {
        String line = new String(buffer, 0, readen, serverInfo.getEncoding());
        logger.trace("Recieved line for "+serverInfo+": "+line);

        stringBuffer.append(line);
        List<Command> parsedCommands = extractCommands(stringBuffer);

        CommandDispatchingThread.getInstance().addCommands(parsedCommands);
    }

    public List<Command> extractCommands(StringBuilder sb) {
        int index = sb.indexOf("|");

        List<Command> parsedCommands = new ArrayList<Command>();

        int lastIndex = 0;
        while (index>-1) {
            String commandStr = sb.substring(lastIndex, index);
            lastIndex = index+1;
            logger.trace("Extracted command for "+serverInfo+": "+commandStr);

            Command command = CommandFactory.getInstance().createCommand(commandStr);
            command.setServerInfo(getServerInfo());
            command.setConnection(this);
            parsedCommands.add(command);

            index = sb.indexOf("|", lastIndex);
        }

        if (lastIndex>0) {
            sb.delete(0, lastIndex);
        }
        return parsedCommands;
    }

    public synchronized void disconnect() throws IOException {
        if (!connected) {
            return;
        }
        try {
            try {
                in.close();
            } catch (IOException e) {
                logger.error("Error while closing output stream", e);
            }
            try {
                out.close();
            } catch (IOException e) {
                logger.error("Error while closing output stream", e);
            }
            socket.close();
        } finally {
            logger.info("Disconnected from "+serverInfo);
            Application.getApplication().getConnectionManager().removeConnection(serverInfo);
            socket = null;
            connected = false;
        }
    }


    public synchronized void reconnect() throws IOException {
        long diff = System.currentTimeMillis();
        if (diff-last< RECONNECTING_INTERVAL) {
            return;
        }
        logger.info("Reconnecting to "+serverInfo);
        disconnect();
        try {
            Thread.sleep(RECONNECTING_INTERVAL);
        } catch (InterruptedException e) {
            logger.error("Error while sleeping before reconnect to server", e);
        }
        if (!terminating) {
            connect();
        }
    }

    public void fireCommandExtracted(Command command) {
        logger.debug("Recieved command: "+command);
        for (CommandListener commandListener : commandListeners) {
            commandListener.commandRecieved(command);
        }
    }



    public boolean isConnected() {
        return connected;
    }

    public void setConnected(boolean connected) {
        this.connected = connected;
    }

    public ServerInfo getServerInfo() {
        return serverInfo;
    }

    public void setServerInfo(ServerInfo serverInfo) {
        this.serverInfo = serverInfo;
    }

    public void addCommandListener(CommandListener commandListener) {
        commandListeners.add(commandListener);
    }

    public synchronized void sendCommand(Command command) {
        String sendStr = command.generateCommand()+"|";
        logger.debug("Sending command: "+sendStr);
        try {
            out.write(sendStr.getBytes());
        } catch (IOException e) {
            logger.error("Can't send command "+sendStr, e);
            if (reconnectOnError) {
                try {
                    reconnect();
                } catch (IOException e1) {
                    logger.error("Can't reconnect to "+serverInfo, e);
                }
            }
        }
    }

    public void chat(String message) {
        final ChatCommand command = new ChatCommand();
        command.setNick(getServerInfo().getNick());
        command.setText(message);
        sendCommand(command);

    }

    public boolean isReconnectOnError() {
        return reconnectOnError;
    }

    public void setReconnectOnError(boolean reconnectOnError) {
        this.reconnectOnError = reconnectOnError;
    }

    protected Socket getSocket() {
        return socket;
    }

    protected void setSocket(Socket socket) {
        this.socket = socket;
    }

    public void close() {
        setReconnectOnError(false);
        terminating = true;
        try {
            disconnect();
        } catch (IOException e) {
            logger.error("Can't close connection:" +serverInfo, e);
        }
    }
}
